package com.zoltu.GoogleAuthenticator;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;

/*
 * TODO: Investigate if it's possible to have the Authenticator create an intent for this service instead of an intent
 * to the activity. Then have this service create the activity notification if it's needed.
 */
/**
 * A service to start when an account needs to be added to the AccountManager. This class will deal with authenticating
 * the username / password (provided in the intent under KEY_USERNAME and KEY_PASSWORD) and if KEY_REPROMPT in the
 * intent is true and authentication failed reprompt the user for credentials.
 * 
 * Upon either successful, failed, or canceled authentication the AccountAuthenticatorResponse (found in the intent
 * under the key KEY_ACCOUNT_AUTHENTICATOR_RESPONSE) will be filled in which will notify the authenticator of the
 * result.
 * 
 * If authentication is a success, call AccountManager.addAccountExplicitly so the new account is added to the
 * AccountManager.
 * 
 * This service will be set to run in the foreground which means a notification to the user will be present as long as
 * it's running.
 **/
public class AuthenticatorService extends Service implements Handler.Callback
{
	/** Intent action to use when calling this service with the intent of adding a new user. **/
	public static final String ACTION_ADD_USER = "com.zoltu.GoogleAuthenticator.add_user";
	/** Intent action to use when calling this service with the intent of canceling a pending authentication. **/
	public static final String ACTION_CANCEL_AUTHENTICATION = "com.zoltu.GoogleAuthenticator.cancel_authentication";
	/** Intent action to use when calling this service to register a new active authentications listener. **/
	public static final String ACTION_REGISTER_AUTHENTICATIONS_LISTENER = "com.zoltu.GoogleAuthenticator.register_authentications_listener";
	/** Intent action to use when calling this service to unregister an active authentications listener. **/
	public static final String ACTION_UNREGISTER_AUTHENTICATIONS_LISTENER = "com.zoltu.GoogleAuthenticator.unregister_authentications_listener";
	
	/** Intent key to a String containing the username. **/
	public static final String KEY_USERNAME = AccountManager.KEY_ACCOUNT_NAME;
	/** Intent key to a String containing the password. **/
	public static final String KEY_PASSWORD = AccountManager.KEY_PASSWORD;
	/** Intent key to a boolean set to true if the user should be prompted again upon authentication failure. **/
	public static final String KEY_REPROMPT = "reprompt";
	/**
	 * Intent key to the AccountAuthenticatorResponse that needs to have it's onResult function called once
	 * authentication is complete (success or failure).
	 **/
	public static final String KEY_ACCOUNT_AUTHENTICATOR_RESPONSE = AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE;
	/** Intent key to an int that contains an error code. **/
	public static final String KEY_ERROR_CODE = AccountManager.KEY_ERROR_CODE;
	/** Intent key to a String that contains an error message. **/
	public static final String KEY_ERROR_MESSAGE = AccountManager.KEY_ERROR_MESSAGE;
	/** Intent key to a Messenger that the service should announce changes to regarding the active authentications. **/
	public static final String KEY_MESSENGER = "messenger";
	
	/** Message.what value for authentication status announcement. **/
	public static final int WHAT_STATUS_ANNOUNCE = 0x0BF4107E;
	
	/** Notification ID for the foreground notification message letting the user know something is authenticating. **/
	private static final int NOTIFICATION_ID_AUTHENTICATING = 0x066FA0EB;
	
	/** The handler for this service's process. **/
	private Handler mHandler;
	
	/**
	 * A map of IntentProcessorThread IDs to the handlers for those threads. Once this map is empty the service can be
	 * safely stopped.
	 **/
	private Map<Integer, Handler> mThreadHandlerMap;
	
	@Override public void onCreate()
	{
		super.onCreate();
		
		/* Create a handler for this service's message queue. */
		mHandler = new Handler(this);
	}
	
	/**
	 * As per android.accounts.AbstractAccountAuthenticator documentation, onBind returns an instance of an
	 * AbstractAccountAuthenticator if the intent that is requesting the binding has the action
	 * AccountManager.ACTION_AUTHENTICATOR_INTENT.
	 **/
	@Override public IBinder onBind(Intent pIntent)
	{
		if (pIntent.getAction().equals(AccountManager.ACTION_AUTHENTICATOR_INTENT))
		{
			Authenticator lAuthenticator = new Authenticator(this);
			return lAuthenticator.getIBinder();
		}
		
		return null;
	}
	
	@Override public int onStartCommand(Intent pIntent, int pFlags, int pStartId)
	{
		/**
		 * Return START_NOT_STICKY so that the service doesn't try to automatically restart itself if it is shutdown due
		 * to memory pressure. Running the service in the foreground whenever critical work is being done (such as
		 * authentication in a thread) should keep the service from going away.
		 * 
		 * TODO: Investigate changing this to START_STICKY. For now let's hope that running in the foreground is enough,
		 * however if the service still gets shut down due to memory pressure even with that then changing it to
		 * START_STICKY will allow recovery once the system has the free resources to try again. Of course, the work
		 * necessary in order to ensure synchronization is non-trivial so hopefully we can avoid having to do this.
		 **/
		final int RETURN_VALUE = START_NOT_STICKY;
		
		if (pIntent.getAction().equals(ACTION_CANCEL_AUTHENTICATION))
		{
			SpawnWorkerThread(new CanceledThread(pIntent));
		}
		else if (pIntent.getAction().equals(ACTION_ADD_USER))
		{
			SpawnWorkerThread(new AddAccountThread(pIntent));
		}
		else if (pIntent.getAction().equals(ACTION_REGISTER_AUTHENTICATIONS_LISTENER))
		{
			/* Get the Messenger from the intent. */
			Messenger lMessenger = (Messenger) pIntent.getParcelableExtra(KEY_MESSENGER);
			/* Register this messenger as a listener. */
			RegisterAuthenticationsListener(lMessenger);
		}
		else if (pIntent.getAction().equals(ACTION_UNREGISTER_AUTHENTICATIONS_LISTENER))
		{
			/* Get the Messenger from the intent. */
			Messenger lMessenger = (Messenger) pIntent.getParcelableExtra(KEY_MESSENGER);
			/* Unregister this messenger as a listener. */
			UnregisterAuthenticationsListener(lMessenger);
		}
		
		return RETURN_VALUE;
	}
	
	/** Called when a message is posted to this thread (usually from an IntentProcessorThread). **/
	@Override public boolean handleMessage(Message pMessage)
	{
		switch (pMessage.what)
		{
			case IntentProcessorThread.WHAT_THREAD_FINISHED:
			{
				ThreadFinished(pMessage.arg1);
				return true;
			}
		}
		
		return false;
	}
	
	/** Called when we receive an intent to do some kind of authentication. **/
	private void SpawnWorkerThread(IntentProcessorThread pThread)
	{
		/* Move the service to the foreground any time we are spawning an authentication thread. */
		StartForeground();
		
		/* Start the thread. */
		pThread.start();
		
		/* Add the thread handler to a map of thread handlers, so we can communicate with the currently active threads. */
		Handler lHandler = pThread.GetHandler();
		mThreadHandlerMap.put(pThread.getThreadId(), lHandler);
	}
	
	/** Called when a message is received telling us that an IntentProcessorThread has finished. **/
	private void ThreadFinished(int pThreadId)
	{
		/* Remove the handler from the map. */
		mThreadHandlerMap.remove(pThreadId);
		
		/* Attempt to move out of the foreground. */
		StopForeground();
	}
	
	/** Called when we receive an intent to register a new authentications listener. **/
	private void RegisterAuthenticationsListener(Messenger pMessenger)
	{
		/* Tell the active authentications about this messenger so they can keep it updated. */
		for (Handler lHandler : mThreadHandlerMap.values())
		{
			Message lMessage = lHandler.obtainMessage(IntentProcessorThread.WHAT_ADD_LISTENER, pMessenger);
			lHandler.sendMessage(lMessage);
		}
	}
	
	/** Called when we receive an intent to unregister an authentications listener. **/
	private void UnregisterAuthenticationsListener(Messenger pMessenger)
	{
		/* Tell the active authentications to stop updating this messenger. */
		for (Handler lHandler : mThreadHandlerMap.values())
		{
			Message lMessage = lHandler.obtainMessage(IntentProcessorThread.WHAT_REMOVE_LISTENER, pMessenger);
			lHandler.sendMessage(lMessage);
		}
	}
	
	/**
	 * Any time this service is actively authenticating a user, call this to set it up to run in the foreground. This
	 * means that the service will have an item in the notification area and is much less likely to be killed when phone
	 * resources run low.
	 * 
	 * The notification item can be selected by the user in order to view the progress of or cancel any active
	 * authentications.
	 **/
	private void StartForeground()
	{
		/* Set this service to run in the foreground (notification presented to the user) so it doesn't get killed. */
		int lIcon = R.drawable.icon;
		String lTickerText = getResources().getString(R.string.authenticating_notification_message_short);
		long lStartTime = System.currentTimeMillis();
		String lMessage = getResources().getString(R.string.authenticating_notification_message_long);
		
		/*
		 * TODO: Build activity and intent for canceling the currently authenticating accounts. The activity should list
		 * all accounts first and let the user choose which to cancel.
		 */
		// PendingIntent lPendingIntent = PendingIntent.getActivity(context, requestCode, intent, flags);
		
		Notification lNotification = new Notification(lIcon, lTickerText, lStartTime);
		// lNotification.setLatestEventInfo(this, lTickerText, lMessage, lPendingIntent);
		
		startForeground(NOTIFICATION_ID_AUTHENTICATING, lNotification);
	}
	
	/**
	 * Any time this service completes an authentication call this to stop the service from being in the foreground.
	 * This service will verify that there are no other pending authentications so it is safe to call it whenever you
	 * think another check should be done.
	 **/
	private void StopForeground()
	{
		/* If there are still IntentProcessorThreads active then stay in the foreground. */
		if (mThreadHandlerMap.size() != 0)
		{
			return;
		}
		stopForeground(true);
	}
	
	/**
	 * Whenever an Authentication needs to update listeners about it's status one of these objects will be sent to the
	 * listener (Messenger).
	 **/
	public static final class AuthenticationStatusAnnouncement
	{
		public enum Type
		{
			CANCEL,
			ADD_ACCOUNT,
		}
		
		public enum State
		{
			STARTING,
			COMPLETED,
			CANCELING,
			AUTHENTICATING,
		}
		
		/** Global UID for this authentication. **/
		public int mId;
		/** The type of authentication that is occurring. **/
		public Type mType;
		/** The current state of authentication. **/
		public State mState;
		/** The account name that is being authenticated (null if none available). **/
		public String mAccountName;
		
		public AuthenticationStatusAnnouncement(int pId, Type pType, State pState, String pAccountName)
		{
			mType = pType;
			mState = pState;
			mAccountName = pAccountName;
		}
	}
	
	/**
	 * Abstract HandlerThread for threads that are going to be processing an intent. Each intent that starts this
	 * service will be processed in it's own thread.
	 **/
	private abstract class IntentProcessorThread extends HandlerThread implements Handler.Callback
	{
		/** The intent that this thread is servicing. **/
		protected final Intent mIntent;
		/** The AccountAuthenticatorResponse this thread needs to respond to before it dies. **/
		protected final AccountAuthenticatorResponse mResponse;
		
		/** Handler for this thread's message loop. **/
		private Handler mHandler;
		/** Handler for the outer class's message loop. **/
		private final Handler mServiceHandler;
		
		/** Messengers that are interested in receiving status updates about this thread's work. **/
		protected final Set<Messenger> mListeners;
		
		/** Message.what value for a message sent by the thread when the thread has finished. **/
		public static final int WHAT_THREAD_FINISHED = 0x00A328C2;
		/**
		 * Message.what value indicating that the thread should abort it's processing and send out a user canceled
		 * AccountAuthenticatorResponse.
		 **/
		public static final int WHAT_USER_CANCELED_THREAD = 0x1AE256EF;
		/**
		 * Message.what value indicating that this thread should add the provided Messenger (Messeng.obj) as a listener.
		 **/
		public static final int WHAT_ADD_LISTENER = 0x39636332;
		/**
		 * Message.what value indicating that this thread should remove the provided Messenger (Message.obj) as a
		 * listener.
		 **/
		public static final int WHAT_REMOVE_LISTENER = 0x25BAABC5;
		
		/**
		 * @param pFriendlyAction
		 *            String to prefix the account name with when displaying this thread to the user.
		 * @param pAccountName
		 *            Account name associated with this thread to display to the user.
		 * @param pIntent
		 *            The intent that this thread is fulfilling.
		 **/
		protected IntentProcessorThread(String pFriendlyAction, String pAccountName, Intent pIntent)
		{
			super(pFriendlyAction + pAccountName);
			
			mIntent = pIntent;
			mResponse = mIntent.getParcelableExtra(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
			mHandler = null;
			mServiceHandler = AuthenticatorService.this.mHandler;
			mListeners = new HashSet<Messenger>();
		}
		
		@Override public boolean handleMessage(Message pMessage)
		{
			switch (pMessage.what)
			{
				case WHAT_USER_CANCELED_THREAD:
				{
					OnUserCanceled();
					return true;
				}
				case WHAT_ADD_LISTENER:
				{
					AddListener((Messenger) pMessage.obj);
					return true;
				}
				case WHAT_REMOVE_LISTENER:
				{
					RemoveListener((Messenger) pMessage.obj);
					return true;
				}
			}
			return false;
		}
		
		/**
		 * Get the handler for this thread.
		 * 
		 * WARNING: This will block until the looper is ready so do not call this until after you have called start() or
		 * else you will deadlock!
		 **/
		public final Handler GetHandler()
		{
			if (mHandler == null)
			{
				mHandler = new Handler(getLooper(), this);
			}
			
			return mHandler;
		}
		
		/**
		 * Derived classes should call this when they are done working in the thread. It will finish the looper (which
		 * will kill the thread) and it will send a message to the main thread letting it know that we are done working
		 * so it can do the appropriate cleanup.
		 **/
		protected final void FinishThread()
		{
			/* Tell any listeners that we are going away. */
			SendStatusAnnouncement();
			
			/* Post a message to quit the looper. */
			quit();
			
			/* Post a message to the main thread letting it know that our work is done and we are quitting. */
			Message lMessage = mServiceHandler.obtainMessage(WHAT_THREAD_FINISHED, getThreadId(), 0);
			mServiceHandler.sendMessage(lMessage);
		}
		
		/**
		 * Helper method for sending a user canceled error response to the AccountAuthenticatorResponse for this thread.
		 **/
		protected final void RespondWithUserCanceled()
		{
			mResponse.onError(AccountManager.ERROR_CODE_CANCELED, getResources().getString(R.string.error_user_canceled));
		}
		
		/** Called when we received a message indicating the user has requested to cancel the authentication. **/
		protected abstract void OnUserCanceled();
		
		/**
		 * When this is called, implementations need to return a populated AuthenticationStatusAnnouncement that can be
		 * sent to a listener.
		 **/
		protected abstract AuthenticationStatusAnnouncement GetStatusAnnouncement();
		
		/** Send a status update to all listeners. **/
		protected void SendStatusAnnouncement()
		{
			for (Messenger lMessenger : mListeners)
			{
				SendStatusAnnouncement(lMessenger);
			}
		}
		
		/** Send a status update to pMessenger. **/
		private void SendStatusAnnouncement(Messenger pMessenger)
		{
			/* Build the message to send the status announcement in. */
			Message lMessage = Message.obtain();
			lMessage.what = WHAT_STATUS_ANNOUNCE;
			lMessage.obj = GetStatusAnnouncement();
			
			/* Send the status announcement. */
			try
			{
				pMessenger.send(lMessage);
			}
			catch (RemoteException pException)
			{
				pException.printStackTrace();
			}
		}
		
		/** Add pMessenger to the listeners to update about the status of this thread. **/
		private void AddListener(Messenger pMessenger)
		{
			mListeners.add(pMessenger);
			
			/* Send pMessenger an initial status message. */
			SendStatusAnnouncement(pMessenger);
		}
		
		/** Remove pMessenger from the listeners to this thread. **/
		private void RemoveListener(Messenger pMessenger)
		{
			mListeners.remove(pMessenger);
		}
	}
	
	/** Thread to do the work of canceling authentication. This is the most simple IntentProcessorThread implementation. **/
	private class CanceledThread extends IntentProcessorThread
	{
		private final AuthenticationStatusAnnouncement mAnnouncement;
		
		public CanceledThread(Intent pIntent)
		{
			super(getResources().getString(R.string.friendly_action_canceled), getResources().getString(R.string.unknown_account_name), pIntent);
			
			int lId = getThreadId();
			AuthenticationStatusAnnouncement.Type lType = AuthenticationStatusAnnouncement.Type.CANCEL;
			AuthenticationStatusAnnouncement.State lState = AuthenticationStatusAnnouncement.State.STARTING;
			String lAccountName = null;
			mAnnouncement = new AuthenticationStatusAnnouncement(lId, lType, lState, lAccountName);
		}
		
		@Override protected void onLooperPrepared()
		{
			super.onLooperPrepared();
			
			/* Set and kick off the response to whatever initiated this request with the provided error bundle. */
			mResponse.onError(mIntent.getIntExtra(KEY_ERROR_CODE, 0), mIntent.getStringExtra(KEY_ERROR_MESSAGE));
			
			/* Update the status announcement. */
			mAnnouncement.mState = AuthenticationStatusAnnouncement.State.COMPLETED;
			
			/* End this thread. */
			FinishThread();
		}
		
		@Override protected void OnUserCanceled()
		{
		/*
		 * Because this thread doesn't actually do any work we just ignore this message if we get it for some reason.
		 * This thread shouldn't live for more than a millisecond or two anyway.
		 */
		}
		
		@Override protected AuthenticationStatusAnnouncement GetStatusAnnouncement()
		{
			return mAnnouncement;
		}
	}
	
	/** Thread to do the actual grunt work of authenticating and adding a new account. **/
	private class AddAccountThread extends IntentProcessorThread
	{
		private final AuthenticationStatusAnnouncement mAnnouncement;
		
		public AddAccountThread(Intent pIntent)
		{
			super(getResources().getString(R.string.friendly_action_add_account), pIntent.getStringExtra(KEY_USERNAME), pIntent);
			
			int lId = getThreadId();
			AuthenticationStatusAnnouncement.Type lType = AuthenticationStatusAnnouncement.Type.ADD_ACCOUNT;
			AuthenticationStatusAnnouncement.State lState = AuthenticationStatusAnnouncement.State.STARTING;
			String lAccountName = pIntent.getStringExtra(KEY_USERNAME);
			mAnnouncement = new AuthenticationStatusAnnouncement(lId, lType, lState, lAccountName);
		}
		
		@Override protected void onLooperPrepared()
		{
			super.onLooperPrepared();
			
			/*
			 * TODO: Kick off authentication. Likely this means setting up the network connection and sending off the
			 * initial request. Don't block here, instead spawn a networking thread if needed to do blocking operations
			 * or check push a message onto the stack to check for network data every so often (each loop or per time
			 * passed).
			 */

			/* Announce that we are authenticating. */
			mAnnouncement.mState = AuthenticationStatusAnnouncement.State.AUTHENTICATING;
			SendStatusAnnouncement();
		}
		
		@Override protected void OnUserCanceled()
		{
			/* TODO: Cleanup any networking stuff and/or spawned threads before finishing the thread. */

			/* Announce that we are canceling. */
			mAnnouncement.mState = AuthenticationStatusAnnouncement.State.CANCELING;
			SendStatusAnnouncement();
			
			/* Send off the user canceled response. */
			RespondWithUserCanceled();
			
			/* Update the status announcement. */
			mAnnouncement.mState = AuthenticationStatusAnnouncement.State.COMPLETED;
			
			/* End this thread. */
			FinishThread();
		}
		
		@Override protected AuthenticationStatusAnnouncement GetStatusAnnouncement()
		{
			return mAnnouncement;
		}
	}
}
